“A graphical summary of various pieces of important information, typically used to give an overview of a business.”
Use Cases:
Businesses
Hospitals
Schools
Research Organizations
Dashboards are subject to frequent data updates with consistent visualizations. This makes them widely used and highly influential analytic tools for decision makers.
On January 24th, 2024, Posit released Quarto 1.4, which introduced Quarto Dashboards that aim to streamline the creation of interactive dashboards, giving you an effortless way to lay out interactive components, visualizations, tabular data, and annotations. Dashboarding before 1.4 was possible, but with limitations (WUSS-2023-Paper-165.pdf).
This paper is not an comprehensive introduction to dashboards, but will focus on the production of high-quality dashboards using Quarto 1.4, a free and open-source tool.
A Sneak Peak
An open-source, plotly-interactable dashboard with Quarto 1.4.
packages <-c("tidyverse", "gt", "quarto")if (any(!sapply(packages, requireNamespace, quietly =TRUE))) {install.packages(packages[!sapply(packages, requireNamespace, quietly =TRUE)])}library(tidyverse) # Data wrangling & visualizationlibrary(gt) # Gold-standard table generationlibrary(quarto) # Open-source publishing system for dashboard generaiton
RStudio®
We utilized RStudio® as the Integrated Development Environment (IDE). A Quarto Project should be setup (.Rproj) with any required subfolders. The Quarto document should be renamed “index.qmd”. Do not use a standard R script.
Before we proceed…
The Data
Publicly available dataset that has been extracted from the Aggregate Analysis of ClinicalTrials.gov (AACT) database (“Clinical Trials Transformation Initiative” n.d.).
Represents all the clinical trials registered and completed within the state of Florida of the United States between 23MAR2014-23MAR2024 (10 years).
The AACT database updates daily, and thus this dataset, and its dashboard, would need to be continuously updated.
This dataset currently includes 73,500 observations of 39 variables, relating to both the protocol designs and the completion of the clinical trials.
All conditions and interventions are included for analysis, and the dataset was cleaned and wrangled by the authors prior to dashboarding.
Before we proceed…
The Data
'data.frame': 73500 obs. of 39 variables:
$ nct_id : Factor w/ 14334 levels "NCT00002495",..: 2835 2835 1608 1608 3090 3090 3090 12902 12902 12902 ...
$ start_date : Date, format: "2013-04-30" "2013-04-30" ...
$ completion_date : Date, format: "2014-09-30" "2014-09-30" ...
$ study_type : Factor w/ 4 levels "Expanded Access",..: 2 2 2 2 2 2 2 2 2 2 ...
$ acronym : chr NA NA NA NA ...
$ baseline_population : chr NA NA NA NA ...
$ brief_title : chr "Impact of SBI, a Medical Food, on Nutritional Status in Patients With HIV-associated Enteropathy" "Impact of SBI, a Medical Food, on Nutritional Status in Patients With HIV-associated Enteropathy" "Extension Study for Patients Who Have Participated in a BMN 701 Study" "Extension Study for Patients Who Have Participated in a BMN 701 Study" ...
$ official_title : chr "Randomized, Double-blind, Placebo-controlled, Multicenter, Clinical Study Evaluating the Impact of Serum-derive"| __truncated__ "Randomized, Double-blind, Placebo-controlled, Multicenter, Clinical Study Evaluating the Impact of Serum-derive"| __truncated__ "A Long-Term Study for Extended BMN 701 Treatment of Patients With Pompe Disease Who Have Participated in a BMN 701 Study" "A Long-Term Study for Extended BMN 701 Treatment of Patients With Pompe Disease Who Have Participated in a BMN 701 Study" ...
$ overall_status : Factor w/ 11 levels "Active, not recruiting",..: 3 3 9 9 3 3 3 3 3 3 ...
$ phase : Factor w/ 8 levels "Early Phase 1",..: 2 2 5 5 5 5 5 5 5 5 ...
$ enrollment : int 103 103 21 21 64 64 64 458 458 458 ...
$ enrollment_type : Factor w/ 2 levels "Actual","Anticipated": 1 1 1 1 1 1 1 1 1 1 ...
$ source : Factor w/ 2739 levels "1800 Contacts, Inc.",..: 868 868 404 404 2209 2209 2209 1930 1930 1930 ...
$ number_of_arms : Factor w/ 26 levels "1","2","3","4",..: 3 3 3 3 2 2 2 5 5 5 ...
$ why_stopped : chr NA NA "The study was terminated because BioMarin decided to end the overall development program based on competing cor"| __truncated__ "The study was terminated because BioMarin decided to end the overall development program based on competing cor"| __truncated__ ...
$ has_expanded_access : logi FALSE FALSE FALSE FALSE FALSE FALSE ...
$ is_fda_regulated_drug : logi FALSE FALSE FALSE FALSE FALSE FALSE ...
$ is_fda_regulated_device : logi FALSE FALSE FALSE FALSE FALSE FALSE ...
$ site : Factor w/ 13349 levels "\"National Ophthalmic Research Institute Retina Consultants of Southwest Florida\"",..: 11675 NA 12133 11077 9522 10878 7297 5313 5313 9413 ...
$ city : Factor w/ 349 levels "Altamonte","Altamonte Springs",..: 87 188 97 318 182 182 291 18 18 77 ...
$ state : Factor w/ 1 level "Florida": 1 1 1 1 1 1 1 1 1 1 ...
$ zip : Factor w/ 1221 levels "00000","033401",..: 664 600 223 832 420 521 909 752 752 77 ...
$ country : Factor w/ 1 level "United States": 1 1 1 1 1 1 1 1 1 1 ...
$ number_of_facilities : int 10 10 12 12 13 13 13 63 63 63 ...
$ actual_duration : int 9 9 61 61 4 4 4 8 8 8 ...
$ were_results_reported : logi TRUE TRUE TRUE TRUE FALSE FALSE ...
$ has_us_facility : logi TRUE TRUE TRUE TRUE TRUE TRUE ...
$ has_single_facility : logi FALSE FALSE FALSE FALSE FALSE FALSE ...
$ minimum_age_num : int 18 18 13 13 18 18 18 18 18 18 ...
$ maximum_age_num : int NA NA NA NA 75 75 75 NA NA NA ...
$ minimum_age_unit : Factor w/ 12 levels "Day","Days","Hour",..: 12 12 12 12 12 12 12 12 12 12 ...
$ maximum_age_unit : Factor w/ 7 levels "Days","Hours",..: NA NA NA NA 7 7 7 NA NA NA ...
$ number_of_primary_outcomes_to_measure : int 1 1 3 3 1 1 1 1 1 1 ...
$ number_of_secondary_outcomes_to_measure: int 0 0 9 9 2 2 2 6 6 6 ...
$ number_of_other_outcomes_to_measure : int 5 5 0 0 0 0 0 0 0 0 ...
$ condition_name : Factor w/ 8501 levels "2019 Novel Coronavirus Disease",..: 3547 3547 5906 5906 2288 2288 2288 5148 3317 5148 ...
$ condition_name_lower_case : Factor w/ 8409 levels "2019 novel coronavirus disease",..: 3509 3509 5839 5839 2262 2262 2262 5097 3281 5097 ...
$ duration_months : num 17 17 60.81 60.81 8.03 ...
$ duration_years : num 1.419 1.419 5.068 5.068 0.666 ...
Quarto 1.4 Syntax
The Basics
Terms to Know
Dashboard – a snapshot of information, typically a high-level overview that can be narrowed-down, or “drilled down” to review specific selections of data.
StaticDashboard – dashboards that provided a fixed view of data, without frequent updates and frequently without the ability to drill-down.
DynamicDashboard – dashboards that are updated frequently, and often allow users to drill-down and interact with the data.
Report – a comprehensive document, typically in extreme detail that has subsections for each topic of interest.
Quarto can generate all these formats (and many more), but we will focus on dashboards specifically.
The Basics
Languages
Dashboards written with Quarto 1.4+ can utilize:
Python (via Jupyter notebooks - .ipynb)
R (via plain text markdown - .qmd) - used for the purposes of this tutorial..
Julia
Observable (“Quarto - Quarto Dashboards” n.d.).
Quarto dashboards can include various mixes of these languages as required by the user/company. Therefore, you could create a dashboard that includes both ggplot2 and matplotlib visualizations.
The Basics
Quarto Dashboard Components
Navigation Bar – serves to provide navigation between pages of the dashboard (if more than one exists).
Pages – dashboards have traditionally been defined as a single page. However, with more complex datasets, it may be useful to divide the data into pages of a dashboard, each telling its own story.
Cards (set with rows/columns) – cards are the fundamental unit of quarto dashboards and are the containers of all data elements (tables, figures, markdown). The content of cards maps to the cells of a notebook or markdown document. Cells that do not produce output are not considered cards. This advancement leads to easier location specification compared to the referenced paper.
Rows/Columns (defined by markdown headings) – rows and columns are used to define where each data element (tables, figures markdown) is positioned on the dashboard.
Tabsets – these are groups of rows or columns on a dashboard that can be used to further divide content.
Sidebars and Toolbars – sidebars are collapsible vertical panels for inputs while toolbars are horizontal panels for inputs. This is where filters, tick boxes, and other drill-down tools can be placed.
The syntax associated with markdown is also outside the scope of this paper, but there are many free and publicly available resources to help with writing markdown syntax (Quarto – Markdown Basics).
Our First Quarto Dashboard
The First Dashboard
Goals
Visualize how many clinical trials are successfully completed each year in the state of Florida.
Visualize the phase distribution of these clinical trials, which may help determine how many patients are receiving care via clinical research (larger numbers of participants are required for higher phase studies, especially phase 3 and phase 4).
Visualize the top 10 conditions that are being successfully studied in Florida.
We will organize the three visuals into one page with three cards, divided into one column with two rows.
We want chart 1 (the trials completed per year) to be emphasized more, while the other two charts supplement our understanding of chart 1.
The First Dashboard
YAML Header
---title:"Dashboarding with Quarto"author:"Joshua J. Cook, Kirk Paul Lafler"format: dashboard: logo: images/logo.png nav-buttons: [linkedin, github]embed-resources: true ---
The First Dashboard
Content
# Page 1## Row 1 - Big Line Plot {height="70%"}#| title: "Annual CT Completion in FL"#| padding: 0pxclinical_trials_fl_yearly_summary <- clinical_trials_fl %>%mutate(year =year(completion_date)) %>%group_by(year) %>%summarise(count =n_distinct(nct_id)) %>%filter(year >=2014& year <=2023)# Extract the 7th color from the "Oranges" paletteoranges_palette <-brewer.pal(8, "Oranges")dark_orange <- oranges_palette[7]# Line chartggplot(clinical_trials_fl_yearly_summary, aes(x = year, y = count)) +geom_line(color = dark_orange, size =1) +# Specify the color and size of the linegeom_point(color = dark_orange, size =2) +# Specify the color and size of the pointsscale_x_continuous(breaks =2014:2023) +# Ensure all years are shownlabs(x ="Year",y ="Successfully Completed Clinical Trials") +theme_minimal() # Use a minimal theme for aesthetics## Row 2 - Two small bar and pie plots {height="30%"}#| title: "Phases of CT Completed in FL"#| padding: 0pxclinical_trials_fl_phase_summary <- clinical_trials_fl %>%distinct(nct_id, .keep_all =TRUE) %>%# Keep only unique NCT_IDscount(phase) # Count the number of NCT_IDs for each phaseclinical_trials_fl_phase_summary <- clinical_trials_fl_phase_summary %>%mutate(across(where(is.character), ~na_if(., "Not Applicable")))# Bar chartggplot(clinical_trials_fl_phase_summary, aes(x = phase, y = n, fill = phase)) +geom_bar(stat ="identity") +# Use identity stat to plot counts directlyscale_fill_brewer(palette ="Oranges") +labs(x ="Phase",y ="Successfully Completed Clinical Trials") +theme_minimal() +# Minimal theme for aestheticstheme(axis.text.x =element_text(angle =45, hjust =1)) # Rotate x-axis labels for readability#| title: "Top 10 Conditions of CT Completed in FL"#| padding: 0pxclinical_trials_fl_condition_summary <- clinical_trials_fl %>%group_by(condition_name) %>%summarise(n =n_distinct(nct_id)) %>%ungroup() %>%arrange(desc(n)) %>%top_n(10, n) # Adjust this to include more or fewer conditions# 2. Create a pie chartggplot(clinical_trials_fl_condition_summary, aes(x ="", y = n, fill = condition_name)) +geom_bar(width =1, stat ="identity", color ="white") +coord_polar(theta ="y") +# This converts the bar chart to a pie chartscale_fill_brewer(palette ="Oranges") +# Apply the Oranges palettelabs(x =NULL,y =NULL,fill ="Condition") +theme_minimal() +theme(axis.line =element_blank(),axis.text =element_blank(),axis.ticks =element_blank(),axis.title =element_blank(),panel.grid =element_blank())
The First Dashboard
Output (HTML)
Beyond the Basics
The Final Dashboard
Advanced Quarto Dashboard Components
Fill v. Flow – instead of defining explicit dimensions for charts (like we did with height above), we can allow charts to fill the available space {.fill} which is the default behavior, or flowing to its natural height {.flow}. Plots in Shiny dashboards dynamically resize.
Scrolling – for very large dashboards, scrolling could be enabled, but it may also be useful to consider pagination or tabsets.
Padding – padding (i.e., white space) can be added to charts to make them more distinct from surrounding content. We disabled padding in the example above.
Interactivity – this can be achieved using a combination of htmlwidgets (which is utilized by packages such as Plotly, Leaflet, and DT), as well as Shiny. This applies to both figures and tables where drill-down, searchability, or dynamic resizing is preferred.
Value Boxes – value boxes are hallmarks of traditional dashboards, frequently being utilized to describe single values such as key performance indicators or counts of an important variable. Value boxes are available in a variety of formats in Quarto 1.4 and can be customized with colors and icons. Importantly, the single value is pulled from a flat R value that will need to be extracted from the dataframe (shown in the code below).
Content is Context – markdown content can be added anywhere in a dashboard using the {.card} div, and can even be added within a chart itself. This content can be dynamic by referencing in-line code. Titles of charts can also be dynamic in the same way.
Multiple Card Output – if a card contains multiple charts, the output can be organized using knitr options layout-ncol and layout-nrow
Theming – Quarto dashboards have access to all the themes available to other quarto documents and can be defined in the YAML header. All available themes can have their components (font, font size, color, table of contents, etc.) customized using Sass files (.scss).
Parameters – computational parameters can be set using knitr (in this case) or Jupyter. For knitr, this is defined in the YAML. Setting parameters allows for reports to be generated for specific subsets of a variable. For example, generating three versions of the dashboard based on each subset of the study_type variable.
The Final Dashboard
Goals
Allow all figures to fill the entire card space(code not shown because that is the default behavior).
Enable scrolling.
Add a small amount of padding to each chart.
Convert our ggplot2 figures to Plotly(using the ggplotly()function for some figures, but this did not always work as shown in the code).
Add a top row with three value boxes to describe the study statuses.
Add some context using markdown.
Apply the united theme.
Add custom navigation buttons:
The Final Dashboard
YAML Header
---title:"Dashboarding with Quarto"author:"Joshua J. Cook, Kirk Paul Lafler"format: dashboard: theme: united scrolling: true logo: images/logo.png nav-buttons:- website- icon: person-raised-hand href: https://joshua-j-cook-portfolios.netlify.app/- li- icon: linkedin href: https://www.linkedin.com/in/KirkPaulLafler/- githubembed-resources: true ---
The Final Dashboard
Content
library(RColorBrewer) # For more colorslibrary(plotly) # For interactivityclinical_trials_fl %>%select(nct_id, overall_status) clinical_trials_fl_completed <- clinical_trials_fl %>%filter(overall_status =="Completed") %>%summarise(total_unique_completed =n_distinct(nct_id)) %>%pull(1)clinical_trials_fl_recruiting <- clinical_trials_fl %>%filter(overall_status =="Recruiting") %>%summarise(total_unique_completed =n_distinct(nct_id)) %>%pull(1)clinical_trials_fl_terminated <- clinical_trials_fl %>%filter(overall_status =="Terminated") %>%summarise(total_unique_completed =n_distinct(nct_id)) %>%pull(1) # Page 1## Row 1 - Value Boxes#| content: valuebox#| title: "Total Trials Completed"list(icon ="person-check-fill",color ="success",value = clinical_trials_fl_completed)#| content: valuebox#| title: "Total Trials Recruiting"list(icon ="person-fill-add",color ="warning",value = clinical_trials_fl_recruiting)#| content: valuebox#| title: "Total Trials Terminated"list(icon ="person-fill-x",color ="danger",value = clinical_trials_fl_terminated)## Row 2 - Big Line Plot::: {.card title=" Annual Clinical Trial Completion in Florida"}#| title: "Annual Clinical Trial Completion in Florida"#| padding: 10pxclinical_trials_fl_yearly_summary <- clinical_trials_fl %>%mutate(year =year(completion_date)) %>%group_by(year) %>%summarise(count =n_distinct(nct_id)) %>%filter(year >=2014& year <=2023)# Extract the 7th color from the "Oranges" paletteoranges_palette <-brewer.pal(8, "Oranges")dark_orange <- oranges_palette[7]# Line chartgg <-ggplot(clinical_trials_fl_yearly_summary, aes(x = year, y = count)) +geom_line(color = dark_orange, size =1) +# Specify the color and size of the linegeom_point(color = dark_orange, size =2) +# Specify the color and size of the pointsscale_x_continuous(breaks =2014:2023) +# Ensure all years are shownlabs(x ="Year",y ="Successfully Completed Clinical Trials") +theme_minimal() # Use a minimal theme for aesthetics# Convert the ggplot object to a plotly objectggplotly(gg)These figures are only for the last 10 years. It is also important to consider the dramatic regulatory changes that occured in2004/2005, and then again in2015/2017, which potentially led to major shifts in registration and results submission compliance for ClinicalTrials.gov. https://aact.ctti-clinicaltrials.org/points_to_consider/:::## Row 3 - Two small bar and pie plots#| title: " Phases of Clinical Trials Completed in Florida"#| padding: 10pxclinical_trials_fl_phase_summary <- clinical_trials_fl %>%distinct(nct_id, .keep_all =TRUE) %>%# Keep only unique NCT_IDscount(phase) # Count the number of NCT_IDs for each phaseclinical_trials_fl_phase_summary <- clinical_trials_fl_phase_summary %>%mutate(across(where(is.character), ~na_if(., "Not Applicable")))# Bar chart; ggplotly() didn't work because of scales, reverted to full plotlyplot_ly(data = clinical_trials_fl_phase_summary, x =~phase, y =~n, type ='bar', color =~phase, colors ="Oranges") %>%layout(xaxis =list(title ="Phase"),yaxis =list(title ="Successfully Completed Clinical Trials"))#| title: "Top 10 Conditions of Clinical Trials Completed in Florida"#| padding: 10pxclinical_trials_fl_condition_summary <- clinical_trials_fl %>%group_by(condition_name) %>%summarise(n =n_distinct(nct_id)) %>%ungroup() %>%arrange(desc(n)) %>%top_n(10, n) # Adjust this to include more or fewer conditions# 2. Create a pie chart; same as aboveplot_ly(clinical_trials_fl_condition_summary, labels =~condition_name, values =~n, type ='pie', textinfo ='label+percent',marker =list(colors = RColorBrewer::brewer.pal(n =8, name ="Oranges")),hole =0.0) %>%layout(title ='Condition Distribution in Clinical Trials',showlegend =TRUE,xaxis =list(showgrid =FALSE, zeroline =FALSE, showticklabels =FALSE),yaxis =list(showgrid =FALSE, zeroline =FALSE, showticklabels =FALSE))
Although this dashboard can be interacted with (i.e., clicking and hovering reveals additional information), it cannot yet be filtered or drilled down, which is a hallmark of modern dashboards.
Advanced Dashboards & Deployment
Advanced Dashboards
Layout Inputs, Drill Down, True Interactivity
There are three ways to add layout inputs (i.e., drill downs or filters) in Quarto, each with their own distinguishing area and background color:
Sidebars – collapsible vertical panels.
Toolbars – collapsible horizontal panels.
Card Inputs – panel for card-specific inputs; these appear in the title area of a card for more condensed organization.
You define all three layout inputs using level two headers, much like rows.
These features can be local to a page, or global across pages (requires a level one heading).
Furthermore, they can be placed on any side of the page and can even be embedded into rows themselves for better clarity on what the input is affecting.
This interactivity is provided by htmlwidgets (R), Jupyter Widgets (Python), or Observable JS (a dialect of JavaScript).
Advanced Dashboards
Shiny
More flexible interactive components and drill down can be added using the following resources:
Shiny for R – for dashboards that use knitr for computations (requires deployment to a server).
Shiny for Python – for dashboards that use Jupyter for computations (requires deployment to a server).
Shiny is beyond the scope of this paper, but resources are attached to learn basic Shiny syntax and deploy a Quarto dashboard using Shiny.
Change the output directory to docs in the quart.yml file.
Add existing repository to GitHub Desktop.
Commit/push changes to your GitHub account.
Find your repository on the GitHub website, navigate to the Settings tab, navigate to Pages. Set Source to “Deploy from a branch” and set branch to “/docs” then save.
After a few moments, the website will render and the link will be listed.
Any future changes can be commited/pushed in the same way, just make sure the HTML is rendered from RStudio® each time you commit. GitHub Actions can automate this.
project: title:" Dashboarding with Quarto 1.4" output-dir: docseditor: visual
Wrapping Up
Tips & Tricks
A Few Things to Remember
Not renaming the quarto markdown file (.qmd) to “index.qmd” can lead to severe issues when rendering and deploying the dashboard. Double check this file name if any issues are encountered. The actual name of the dashboard (that appears on the dashboard itself) can be whatever you want and is defined inside the quarto markdown file.
The GitHub Pages section of the Quarto documentation does not utilize the GitHub Desktop Application (Quarto – GitHub Pages). In fact, most resources refer to terminal commands, but we do not recommend them due to added complexity. Try out the desktop application!
When in doubt, check the Dashboard Examples (see Recommended Reading). Our full code and example are also linked below.
It can help to have some familiarity with Quarto’s other formats before attempting dashboarding. The syntax and structure follow very similar patterns as other formats, such as Quarto websites and reports.
Conclusion
We Hope You Try Quarto 1.4 Dashboards!
The paper demonstrated how Quarto’s cross-language support, combined with its new and improved dashboarding functionality, provides a comprehensive environment for data reporting.
This integration is crucial for many industries where making informed decisions often depends on understanding complex datasets that update over time.
By leveraging Quarto’s new dashboarding tools, analysts from a variety of programming backgrounds can more effectively present their data, leading to improved decision-making processes and clearer communication of findings.
Quarto 1.4 has an extraordinary opportunity to enhance open-source data visualization and reporting practices within a multitude of industries leaning toward open-source software.
Acknowledgements & Contact Information
The authors thanks PharmaSUG for their support through the PharmaSUG New Professionals Scholarship Program. Joshua J. Cook thanks his mentors, Kirk Paul Lafler (@sasNerd), and Dr. Achraf Cohen, for their ongoing mentorship and support.